#Logtrack-ng - Main module
#Copyright (C) yyyy  name of author
#
#This program is free software; you can redistribute it and/or
#modify it under the terms of the GNU General Public License
#as published by the Free Software Foundation; either version 2
#of the License, or (at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

=head1 NAME

Logtrack - modul kontrolujici prichod novych zprav do logu

=head1 DESCRIPTION

Modul se stara o celou rezii programu a obsahuje jeji veskerou konfiguraci.
Modul pro sebe otevre deskriptory (v pripade souboru) a sockety pro prijem
zprav z logu - tyto prichozi zpravy nasledne predava zdedenemu obj. Analyzer.
Modul se rovnez stara o aktualizaci i-uzlu a prectenych znaku v databazi pro
pripadne znovunacteni udaje v pripade vypadku.

=head2 FUNCTIONS
=over 4

=item online_analysis
=item open_logs


=cut

package Logtrack;

use strict;
no strict 'refs';
no strict 'subs';

use Exporter;
use Logtrack::Analyzer;
use XML::Checker::Parser;
use XML::Mini::Document;
use Logtrack::Validator;
use Logtrack::DBI;
use Data::Dumper;
use DBI;
use Exception qw(:all);
use XML::Dumper;
use IO::Socket;
use IO::File;
use IO::Select;
$Data::Dumper::Indent = 1;

use constant TRUE => 1;
use constant FALSE => 0;

our @ISA = qw(Logtrack::Analyzer);

=item
Konstruktor tridy - prebira zdedenou tridu Analyzer. Konstruktor nejprve 
validuje prichozi prepinace a nastaveni prostrednictvim modulu 
Logtrack::Validator a vysledna data si ulozi. Pote otevira spojeni k databazi
pres modul Logtrack::DBI. 

Class constructor - inherits class Analyzer which analyze messages a determines
actions
=cut

sub new {
	my ($self) = @_;
	
	my $logtrack = $self->SUPER::new();
	my $class = ref $self || $self;
	
	my $validator = Logtrack::Validator->new();
	$logtrack->{data} = $validator->validate();
	$logtrack->{dbi} = Logtrack::DBI->new($logtrack->{data});
	$logtrack->check_sources();
		
	
	if($logtrack->{data}{test}) {
		print Dumper($logtrack);
		
		exit(0);
	}
	
	$logtrack->{dns} = {};
	$logtrack->{services} = {};
	$logtrack->open_logs();
	$logtrack->dump_fd();
	for(my $index=0; $index < @{$logtrack->{data}{logs}}; $index++) {
		$logtrack->{dbi}->erase_old(
			$index,$logtrack->{data}{logs}->[$index]->{rotate_time}
		)
	}
	$logtrack->{debug}->debugln('Building module Logtrack.. complete!',2);
	bless ($logtrack, $class);
	return $logtrack;
}

=item online_analysis
First function opens descriptors and number of red characters to configuration
through function open_logs. Then it only checks incomming of new messages
and calls function analyse. After reading all messages in one cycle, it calls
func. dump_fd for updating seek parameter v DB

Funkce neprve nacte deskriptory s poctem prectenych znaku do konfigurace 
prostrednictvim fce open_logs. Pote jen kontroluje prichod nove zpravy a zavola
funci analyse. Po precteni novych zprav v jednom cyklu zavola funkci dump_fd 
pro aktualizaci seek parametru v databazi.
=cut

sub online_analysis {
	my ($self) = @_;
	my ($logfile_size,$logfile_inode,$fd,$sync);
	
	$self->debugln('Executing analysis' ."\n" .'-' x 16,1);
	
	my $reader = new IO::Select();
	my $dump = new XML::Dumper;
	
	foreach my $sock (@{$self->{sockets}})	{
		$reader->add($sock);
	}
	
	while(1) {
		$sync = FALSE;
		for(my $index=0; $index < @{$self->{data}{logs}}; $index++) {
			next if($self->{files}->[$index] eq ''); #Entity is socket 
	
			$logfile_size = ( stat( $self->{files}->[$index]->{fd} ) )[7];
			$logfile_inode = ( stat( $self->{data}{logs}->[$index]->{file} ) )[1];

			if($self->{files}->[$index]->{inode} !=
				$logfile_inode	) {
				$self->debugln('Deskriptor is changed, loading now one',1);

				#There are some unred messages in old rotated logfile,
				#  these messages are processed first	
				if($logfile_size == $self->{files}->[$index]->{seek}) {
					$self->{files}->[$index]->{fd}->close;
					$self->{files}->[$index]->{fd} = 
						IO::File->new($self->{data}{logs}->[$index]->{file});
					$logfile_size = ( stat($self->{files}->[$index]->{fd}))[7];
					$self->{files}->[$index]->{inode} = 
						( stat( $self->{files}->[$index]->{fd} ) )[1];
					$self->{files}->[$index]->{seek} = 0;
				}
			}
			
			next if($logfile_size == $self->{files}->[$index]->{seek});
	
			$fd = $self->{files}->[$index]->{fd};
			if($self->{files}->[$index]->{seek} < $logfile_size) {
				seek($fd, $self->{files}->[$index]->{seek}, 0);
			}
			else {
				seek($fd, 0, 0); 
			}
			
			while(my $line = $fd->getline()) {
				$self->{line} = $line;
				chomp($line);
				$self->debugln('Analysing message: '. $line,3);
				$self->analyse(\$line,\$self->{data}{logs}->[$index]);
			}
				
			#Updating file descriptors
			$self->{files}->[$index]->{seek} = tell($fd);
			$sync = TRUE;
		}

		#Processing sockets - sockets are red by one messages for each socket
		# in one cycle
		select();
		my ($rdr) = IO::Select->select($reader, undef, undef, 0);
		my ($sck,$sock,$buf);
		foreach $sck (@$rdr) {
			my $new_sock = FALSE;
			foreach  $sock (@{$self->{sockets}}) {
				if($sck == $sock)  {
					my $new = $sck->accept;
					$reader->add($new);
					$new_sock = TRUE;
					last;
				}
			}
			next if($new_sock);
			$buf = <$sck>;
			if($buf) {
		
		#Loading proper configuration for actual socket on this particular port
				for(my $index=0; $index < @{$self->{data}{logs}}; $index++) {
					next if($self->{files}->[$index] ne ''); #Neni to socket
					next if($sck->sockport() != 
						$self->{data}{logs}->[$index]->{socket}->{port});
					$self->analyse(\$buf,\$self->{data}{logs}->[$index]);
				}
			}
			#Socket closed it's connection
			else {
				$reader->remove($sck);
				close($sck);
			}
		}
		
		$self->dump_fd() if($sync);
		sleep $self->{data}{refresh};
	}
}

=item open_logs
Function opens file and socket descriptors for receiving messages and stores
them into configuration. If flag reload is set, function update numbers of
reded characters and i-nodes from DB

Funkce otevre soubory a sockety pro prijem zprav a ulozi si jejich deskriptory
do konfigurace. Pokud je nastaven parametr reload - funkce aktualizuje pocet
prectenych znaku a i-uzly z udaju z databaze.
=cut


sub open_logs {
	my $self = shift;
	my ($fd,$file,$file_name,$file_size,$file_inode,@files);
	my ($sock,$socket,@sockets);
	
	$self->debug('Executing open_logs..',3);
	
	
	for(my $index = 0; $index < @{$self->{data}{logs}}; $index++) {
		$file_name = $self->{data}{logs}->[$index]->{file};
		$socket = $self->{data}{logs}->[$index]->{socket};
		
		if( $file_name ne '' && -e $file_name) {
			$fd = IO::File->new($file_name) 
				or die 'Can\'t open file '. "$file_name \n";
			$file_size = ( stat($fd) )[7];
			$file_inode = ( stat($fd) )[1];
			$file->{fd} = $fd;
			$file->{seek} = $file_size;
			$file->{inode} = $file_inode;
			$files[$index] = $file;
			$file = ();
		}
		elsif($socket ne '') {
			$sock = new IO::Socket::INET(Listen => 20, 
							LocalPort => $socket->{port},
							LocalHost => $socket->{host},
							Proto => $socket->{proto},
							Reuse=> 1,);
			push @sockets,$sock;
			$files[$index] = '';
		}
		else {
			print 'Current settings doesn\'t define ' .
				'proper filename nor socket'."\n";
			$files[$index] = '';
		}
	}
	
	
#Pokud je nastaven pripinac extrahuje se z DB i-uzly a pocet prectenych znaku
	if($self->{data}{reload}) {
		my ($ref_db,$ok,$query,$result);
		$ref_db = $self->{dbi}->get_db();
		$query = $$ref_db->prepare('SELECT * FROM file_desc WHERE id = ?');
		for(my $index = 0; $index < @{$self->{data}{logs}}; $index++) {
			$ok = $query->execute($index);
			$result = $query->fetchrow_arrayref;
# 	Existuje vysledek a zaroven si odpovidaji i-uzly - tj. nezrotoval log
			if($result && ($files[$index] ne '') &&
				($files[$index]->{inode} == $result->{inode}) ) {
					$files[$index]->{seek} = $result->{seek};
			}
		}
		if($ok) {
			$$ref_db->commit;
		}
		else {
			$$ref_db->rollback;
		}
	}
	
	$self->{files} = \@files;
	if(@sockets) {
		$self->{sockets} = \@sockets;
	}
	else {
		$self->{sockets} = '';
	}
	
	$self->println("\t" .' done!',3);
}


=item dump_fd
Funkce aktualizuje pocet prectenych znaku a aktualni i-uzly do databaze pro 
pripadne znovunacteni udaju pri reload funkci

Function updates number of reded letters and i-nodes to DB for reloading 
program
=cut


sub dump_fd {
	my ($self) = @_;
	my ($ref_db,$query);
	my $ok = 1;
	$ref_db = $self->{dbi}->get_db();
	if(not defined $ref_db) {
		die 'Database is not defined!';
	}

	$query = $$ref_db->prepare(
		'SELECT insert_fd(?,?,?)');

	for(my $index = 0; $index < @{$self->{files}}; $index++) {
		
		next if($self->{files}->[$index] eq ''); #Sockety se budou preskakovat
		$ok *= $query->execute($index,$self->{files}->[$index]->{seek},
				$self->{files}->[$index]->{inode});
	}

	if($ok) {
		
		$$ref_db->commit;
	}
	else {
		$self->errstr('DB error during dump_fd function'."\n");
		$self->debug('DB error during dump_fd function'."\n");
		$self->check_connection();
		$$ref_db->rollback;
	}
	
}

=item
Function firstly determines limit, where will be stored temporary records.
This limit will be number of existing records in DB plus number of log in 
current configuration. Then it store record if it is avaible, otherwise it 
check whether are same. If not, old config is put beyond the limit. When all new
records are stored, records beyond the limit are erased.

Funkce nejprve urci hranici, ze kterou bude ukladat pripadne prekazejici 
zaznamy - toto maximum bude souctem poctu existujich zaznamu v databazi a poctu
zdroju logu v aktualni konfiguraci. Nasledne funkce zjisti, zda existuje 
zaznam s ID, pokud ano, tak jej premisti za hranici. Rovnez se jeste diva, 
zda uz zaznam se stejnym klicem jiz neexistuje a pokud ano, tak pouze updatuje 
jeho ID na korektni hodnotu.
=cut

sub insert_source {
	my ($self,$id,$key) = @_;
	my $ref_db = $self->{dbi}->get_db();
	my ($max,$result);
	$max = @{$self->{data}{logs}};
	$result = $$ref_db->selectrow_hashref(
		'SELECT MAX(id) + 1 AS max FROM sources',{}
	);

#Vytvori maximalni hranici - za ni se docasne vkladaji prekazejici zaznamy
	if(defined $result->{max}) {
		$max += $result->{max};
	}

	my $result_id = $$ref_db->selectrow_hashref(
		'SELECT key FROM sources WHERE id=?',{},$id
	);
	my $result_key = $$ref_db->selectrow_hashref(
		'SELECT id FROM sources WHERE key=?',{},$key
	);
	
#Zaznam neexistuje a pozadovane misto je volne
	if((not defined $result_id) && (not defined $result_key)) {
		$$ref_db->do('INSERT INTO sources VALUES(?,?,DEFAULT)',{},$id,$key);
	}
# Zaznam exituje a pozadovane misto je volne - dojde pouze ke zmene id
	if( (not defined $result_id) && (defined $result_key)) {
		$$ref_db->do('UPDATE sources SET id = ? WHERE key = ?', {}, $id,$key);
	}
#Zaznam neexistuje, ale pozadovane misto je plne, musi se uvolnit
	if((defined $result_id) && (not defined $result_key)) {
#Docasne presunu obsazene misto mimo hranice ID
		$$ref_db->do('UPDATE sources SET id = ? WHERE id = ?',
		{},$max  + $id, $id);
		$$ref_db->do('INSERT INTO sources VALUES(?,?,DEFAULT)',{},$id,$key);
	}
#Pozadovane misto je plne a zaroven zaznam uz existuje jinde
#Zaznam presuneme za hranici a jiz existujici klic precislujeme
	if((defined $result_id) && (defined $result_key)) {
		return if($result_id->{key} eq $key);
		$$ref_db->do('UPDATE sources SET id = ? WHERE id = ?', 
			{}, $max + $id, $id);
		$$ref_db->do('UPDATE sources SET id = ? WHERE key = ?', {}, $id, $key);
	}
}

=item
Funkce prochazi zdroje logu - vytvori pro ne klic pro tabulku sources a vlozi 
ji do databaze pomoci funkce insert_source, ktera se postara o setrideni 
zaznamu podle skutecnosti. Nasledne pak provede smazani zaznamu, ktere jsou 
mimo cisla logu (tj. zaznamy, ktere jsou jiz neaktualni).
=cut

sub check_sources {
	my ($self) = @_;
	$self->debug('Executing check_sources..',3);
	my $ref_logs = $self->{data}{logs};
	my $ref_db = $self->{dbi}->get_db();
	my ($result,$ok,$source_key,@source_keys);

	for(my $index=0; $index < @{$ref_logs}; $index++) {
		if($ref_logs->[$index]->{file} ne '') {
			$source_keys[$index] = $ref_logs->[$index]->{file};
		}
		else {
			$source_keys[$index] = $ref_logs->[$index]->{socket}->{proto} .' '.
				$ref_logs->[$index]->{socket}->{port};
		}
	}

	for(my $index=0; $index < @{$ref_logs}; $index++ ) {
		
		$source_key = $source_keys[$index];
		$self->insert_source($index,$source_key);
		
	}
	my $max = @{$ref_logs};
	$$ref_db->do('DELETE FROM sources WHERE id >= ?',{}, $max );
	$$ref_db->commit;
	$self->println("\t" .'done!',3);
}

1;
